<?php
use Arhframe\Util\Folder; //this is still from my own framework you can have it on packagist drectly
include_once __DIR__ . '/../model/abstract/AbstractEntitie.php';

/**
 * @author ArthurH
 * Tiny orm for this website
 * you should repect this in your entity:
 * class name is the same name of your table name
 * class properties must be the same of your table fields
 *
 */
class MiniOrm
{
    private $pdo;
    private $cachedEntity = array();
    private $entities = array();
    private static $_instance;

    private function __construct($dns, $user, $password = null, $options = null)
    {
        $this->pdo = new PDO($dns, $user, $password);
    }

    public static function getInstance($dns = null, $user = null, $password = null, $options = null)
    {
        if (is_null(self::$_instance)) {
            if (empty($dns) || empty($user)) {
                throw new Exception("Dns and user must be set for new instance");
            }
            self::$_instance = new MiniOrm($dns, $user, $password, $options);
        }

        return self::$_instance;
    }

    public function addEntitie(AbstractEntitie $entities)
    {
        if (!is_object($entities)) {
            throw new Exception("This is not an object");
        }
        $class = new ReflectionClass($entities);
        $properties = $class->getProperties();
        $infoEntities = array();
        foreach ($properties as $propertie) {

            $infoEntities[] = $propertie->getName();
        }
        $this->entities[strtolower($class->getName())] = array('table' => strtolower($class->getName()), 'values' => $infoEntities, 'map' => array());
    }

    public function load()
    {
        foreach ($this->entities as $tableName => $infos) {
            foreach ($infos['values'] as $key => $value) {
                if (!$this->isEntitie($value)) {
                    continue;
                }

                $this->entities[$tableName]['map'][$this->getRealEntitie($value)] = $this->isMany($value);
                unset($this->entities[$tableName]['values'][$key]);
            }
        }
    }

    private function isEntitie($attr)
    {
        $attr = ucfirst($attr);
        if (!empty($this->entities[$attr])) {
            return true;
        }
        $attrFinal = $attr[strlen($attr) - 1];
        if ($attrFinal == 's') {
            return $this->isEntitie(substr($attr, 0, strlen($attr) - 1));
        }
        return false;
    }

    private function getRealEntitie($attr)
    {
        $attr = ucfirst($attr);
        if (!$this->isMany($attr)) {
            return $attr;
        }
        return substr($attr, 0, strlen($attr) - 1);
    }

    private function isMany($attr)
    {
        $attrFinal = $attr[strlen($attr) - 1];
        if ($attrFinal == 's') {
            return true;
        }
        return false;
    }

    public function getEntityName(AbstractEntitie $entitie)
    {
        $entitie = new ReflectionClass($entitie);
        return strtolower($entitie->getName());
    }

    public function getEntitie(AbstractEntitie $entities)
    {
        if ($entities == null) {
            return null;
        }
        if (!is_object($entities)) {
            throw new Exception("This is not an object");
        }
        $entities = $this->getEntityName($entities);
        if (empty($this->entities[$entities])) {
            throw new Exception("This is not an entitie");
        }
        return $this->entities[$entities];
    }

    public function loadEntitiesByFolder($folderPath)
    {
        $folder = new Folder($folderPath);
        $files = $folder->getFiles('/\.php$/');
        foreach ($files as $file) {
            include_once $file->absolute();
            $entitieName = $file->getBase();
            $this->addEntitie(new $entitieName());
        }
        $this->load();
    }

    public function getEntitieByName($entitieName)
    {
        $entitieName = strtolower($entitieName);
        if (empty($this->entities[$entitieName])) {
            throw new Exception("This is not an entitie");
        }
        return $this->entities[$entitieName];
    }

    public function create(AbstractEntitie $entitie)
    {
        $entitieInfo = $this->getEntitie($entitie);
        $entitieName = $this->getEntityName($entitie);
        $table = $entitieInfo['table'];
        $values = $entitieInfo['values'];

        $map = $entitieInfo['map'];
        $reversedValues = array_flip($values);
        unset($reversedValues['id']);
        $values = array_flip($reversedValues);
        $prepareSql = "INSERT INTO " . $table . " (";
        $prepareSql .= implode(', ', $values);

        foreach ($map as $entitieMappedName => $isMany) {
            if ($isMany) {
                continue;
            }
            $prepareSql .= ", id_" . strtolower($entitieMappedName);
        }
        $prepareSql .= ') VALUES(';
        $prepareSql .= implode(', ', array_fill(0, count($values), "?"));
        foreach ($map as $entitieMappedName => $isMany) {
            if ($isMany) {
                continue;
            }
            $get = 'get' . $entitieMappedName;
            $linkedEntity = $entitie->$get();
            if ($linkedEntity !== null && $linkedEntity->getId() === null) {
                try {
                    $this->create($linkedEntity);
                } catch (Exception $e) {
                    throw new Exception("Error when update entitie '$entitieName' : " . $e->getMessage());
                }
            }
            if ($linkedEntity === null) {
                $prepareSql .= ", DEFAULT";
            } else {
                $prepareSql .= ", " . $linkedEntity->getId();
            }
        }
        $prepareSql .= ')';
        $stmt = $this->pdo->prepare($prepareSql);
        $i = 1;
        foreach ($values as $value) {
            $get = "get" . ucfirst($value);
            $toSend = "toSend$i";
            $$toSend = $entitie->$get();
            $stmt->bindParam($i, $$toSend);
            $i++;
        }
        if (!$stmt->execute()) {
            $error = $stmt->errorInfo();
            throw new Exception("Error when creating entitie '$entitieName' : " . $error[2]);
        }
        try {
            $entitie->setId($this->pdo->lastInsertId());
            $stmt->closeCursor();
            $this->findById($entitieName, $entitie->getId());
        } catch (Exception $e) {
            throw new Exception($e->getMessage());
        }
    }

    private function getCachedEntity($entitieName, $id)
    {
        if (empty($this->cachedEntity[$entitieName][$id])) {
            return null;
        }
        return $this->cachedEntity[$entitieName][$id];
    }

    private function addEntityToCache(AbstractEntitie $entitie)
    {
        $this->cachedEntity[$this->getEntityName($entitie)][$entitie->getId()] = $entitie;
    }

    private function selectRequest($entitieName, $sql, $linkedEntity = null, $filledBy = array())
    {
        $entitieInfo = $this->getEntitieByName($entitieName);
        $table = $entitieInfo['table'];
        $values = $entitieInfo['values'];
        $map = $entitieInfo['map'];
        $query = $this->pdo->query($sql);

        $returnEntities = array();

        while ($data = $query->fetch()) {
            $cachedEntity = $this->getCachedEntity($entitieName, $data['id']);
            if (!empty($cachedEntity)) {
                $entitie = $cachedEntity;
            } else {
                $entitie = new $entitieName();
                $entitie->setId($data['id']);
                $this->addEntityToCache($entitie);
            }


            foreach ($values as $value) {
                if ($value == 'id') {
                    continue;
                }
                $set = 'set' . ucfirst($value);
                $entitie->$set($data[strtolower($value)]);
            }

            foreach ($map as $entitieMappedName => $isMany) {
                if (in_array($entitieMappedName, $filledBy)) {
                    continue;
                }
                if ($isMany) {
                    $attr = $entitieMappedName . 's';
                } else {
                    $attr = $entitieMappedName;
                }
                $set = 'set' . ucfirst($attr);
                $get = 'get' . ucfirst($attr);
                $setLinkedEntity = 'set' . $entitieName;
                if ($isMany) {
                    if (!empty($linkedEntity)) {
                        $get = $entitie->$get();
                        if (!is_array($get)) {
                            $get = array($get);
                        }
                        $get[] = $linkedEntity;
                        $entitie->$set($get);
                        $linkedEntity = null;
                    }
                    $filledBy[] = $entitieName;
                    $linkedEntities = $this->findByFieldPrivate($entitieMappedName, array('id_' . strtolower($table) => $entitie->getId()), null, null, null, $filledBy);
                    if (empty($linkedEntities)) {
                        $linkedEntities = array();
                    }
                    $entitie->$set($linkedEntities);
                    foreach ($linkedEntities as $linkedEntitie) {
                        $linkedEntitie->$setLinkedEntity($entitie);
                    }
                } else {
                    $linkedEntitie = $this->findByFieldPrivate($entitieMappedName, array('id' => $data['id_' . strtolower($entitieMappedName)]), null, null, $entitie);
                    $entitie->$set($linkedEntitie);
                }
            }
            $returnEntities[] = $entitie;
        }
        $query->closeCursor();
        if (count($returnEntities) == 1) {
            return $returnEntities[0];
        }
        if (count($returnEntities) == 0) {
            return null;
        }
        return $returnEntities;
    }

    public function findAll($entitieName)
    {
        $entitieInfo = $this->getEntitieByName($entitieName);
        $table = $entitieInfo['table'];
        return $this->selectRequest($entitieName, "SELECT * FROM $table");
    }

    private function findByFieldPrivate($entitieName, array $fields, $predicate = null, $logic = null, $linkedEntity = null, $filledBy = array())
    {
        $request = "";
        $i = 0;
        if (empty($predicate)) {
            $predicate = "=";
        }
        if (empty($logic)) {
            $logic = "AND";
        }
        foreach ($fields as $field => $value) {
            if ($i > 0) {
                $request .= " $logic ";
            }
            $value = addslashes($value);
            $request .= "$field $predicate '$value'";
            $i++;
        }
        $entitieInfo = $this->getEntitieByName($entitieName);
        $table = $entitieInfo['table'];

        return $this->selectRequest($entitieName, "SELECT * FROM $table WHERE " . $request, $linkedEntity, $filledBy);
    }

    /**
     * @param unknown $entitieName
     * @param array $fields
     * @return multitype:unknown
     *
     * &fields is array with field=>value
     */
    public function findByField($entitieName, array $fields, $predicate = null, $logic = null)
    {
        return $this->findByFieldPrivate($entitieName, $fields, $predicate, $logic);
    }

    public function findById($entitieName, $id, $linkedEntity = null)
    {
        $id = (int)$id;
        return $this->findByField($entitieName, array('id' => $id), $linkedEntity);
    }

    private function removeFromEntitie(AbstractEntitie $entitie, AbstractEntitie $toRemove)
    {
        $entitieInfo = $this->getEntitie($entitie);
        $map = $entitieInfo['map'];
        $attr = $this->getEntityName($toRemove);
        $set = "set" . ucfirst($attr);
        $get = "get" . ucfirst($attr);
        if (!$map[$attr]) {
            $entitie->$set(null);
            return;
        }
        $set .= "s";
        $get .= "s";
        $entities = $entitie->$get();
        $idToRemove = null;
        if (empty($entities)) {
            return;
        }
        foreach ($entities as $key => $entitieItem) {
            if ($entitieItem->getId() == $toRemove->getId()) {
                $idToRemove = $key;
            }
        }
        if ($idToRemove === null) {
            return;
        }
        unset($entities[$idToRemove]);
        $entitie->$set($entities);
    }

    public function update(AbstractEntitie $entitie)
    {
        $entitieInfo = $this->getEntitie($entitie);
        $entitieName = $this->getEntityName($entitie);
        $map = $entitieInfo['map'];
        $table = $entitieInfo['table'];
        $values = $entitieInfo['values'];
        $reversedValues = array_reverse($values);
        unset($reversedValues['id']);
        $values = array_reverse($reversedValues);

        $prepareSql = "UPDATE " . $table . " SET ";
        $i = 0;
        foreach ($values as $value) {
            if ($i > 0) {
                $prepareSql .= ", ";
            }
            $prepareSql .= "$value=?";
            $i++;
        }
        foreach ($map as $entitieMappedName => $isMany) {
            if ($isMany) {
                continue;
            }
            if (empty($this->cachedEntity[$entitieMappedName])) {
                $this->cachedEntity[$entitieMappedName] = array();
            }
            foreach ($this->cachedEntity[$entitieMappedName] as $entitieItem) {
                $this->removeFromEntitie($entitieItem, $entitie);
            }
            $get = 'get' . $entitieMappedName;
            $linkedEntity = $entitie->$get();
            if ($linkedEntity === null) {
                continue;
            }
            if ($linkedEntity !== null && $linkedEntity->getId() === null) {
                try {
                    $this->create($linkedEntity);
                } catch (Exception $e) {
                    throw new Exception("Error when update entitie '$entitieName' : " . $e->getMessage());
                }
            }
            $prepareSql .= ", id_" . strtolower($entitieMappedName) . "=" . $linkedEntity->getId();

        }
        $prepareSql .= " WHERE id=" . $entitie->getId();
        $stmt = $this->pdo->prepare($prepareSql);
        $i = 1;
        foreach ($values as $value) {
            $get = "get" . ucfirst($value);
            $toSend = "toSend$i";
            $$toSend = $entitie->$get();
            $stmt->bindParam($i, $$toSend);
            $i++;
        }
        if (!$stmt->execute()) {
            $error = $stmt->errorInfo();
            throw new Exception("Error when update entitie '$entitieName' : " . $error[2]);
        }
        try {

            $stmt->closeCursor();
            $this->findById($entitieName, $entitie->getId());
        } catch (Exception $e) {
            throw new Exception($e->getMessage());
        }
    }

    public function remove(AbstractEntitie $entitie)
    {
        $entitieInfo = $this->getEntitie($entitie);
        $entitieName = $this->getEntityName($entitie);
        $table = $entitieInfo['table'];
        try {
            $this->pdo->exec("DELETE FROM $table WHERE id=" . $entitie->getId());
        } catch (Exception $e) {
            throw new Exception("Error when delete entitie '$entitieName' : " . $e->getMessage());
        }

    }

    public function getPdo()
    {
        return $this->pdo;
    }
}